﻿using System;
using System.IO;
using System.Collections.Generic;
using NAnt.Core;
using NAnt.Core.Attributes;
using NAnt.Core.Types;

namespace NAntExt.Tasks
{
    /// <summary>
    /// CleanUpTask allows you to conditionally delete files or directories based on
    /// a either a minimum number of files to keep OR based on the age of the files
    /// or folders. A combination can be used.
    /// </summary>
    [TaskName("cleanup")]
    public class CleanUpTask : Task
    {

        private TimeSpan _retryTimeout = System.TimeSpan.FromMinutes(15);

        /// <summary>
        /// The length of time to keep trying to delete a file. Default is 15 minutes.
        /// </summary>
        [TaskAttribute("retryTimeout")]
        [StringValidator(AllowEmpty = false)]
        public string RetryTimeout
        {
            get
            {
                return _retryTimeout.ToString();
            }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    _retryTimeout = TimeSpan.Zero;
                }
                else
                {
                    _retryTimeout = TimeSpan.Parse(value);
                }
            }
        }

        private TimeSpan _maxAge = System.TimeSpan.Zero;

        /// <summary>
        /// Maximum age the files or directories can be before they are deleted. If the creation time minus
        /// today's date is less must be less than this value else the file or directory will be deleted. The format
        /// is based on <see cref="System.TimeSpan"/>. Example 0.00:00:00.000. Defaults to <see cref="System.TimeSpan.Zero"/>
        /// which means it ignore age and it will not be used.
        /// </summary>
        [TaskAttribute("maxage")]
        [StringValidator(AllowEmpty = false)]
        public string MaxAge
        {
            get
            {
                return _maxAge.ToString();
            }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    _maxAge = TimeSpan.Zero;
                }
                else
                {
                    _maxAge = TimeSpan.Parse(value);
                }
            }
        }

        private int _maxCount = int.MaxValue;

        /// <summary>
        /// Maximum number of files or directories to keep based on their creation date. Only the newest files will
        /// be kept. Any files or directories beyond this number will be deleted. Default is <see cref="int.MaxValue"/>.
        /// </summary>
        [TaskAttribute("maxcount", Required = false)]
        [StringValidator(AllowEmpty = false)]
        public int MaxCount
        {
            get
            {
                return _maxCount;
            }
            set
            {
                _maxCount = value;
            }
        }

        private FileSet _cleanUpFileSet = new FileSet();

        /// <summary>
        /// The files or directories to clean up.
        /// </summary>
        [BuildElement("fileset")]
        public virtual FileSet CleanUpFileSet
        {
            get
            {
                return _cleanUpFileSet;
            }
            set
            {
                _cleanUpFileSet = value;
            }
        }

        /// <summary>
        /// Primary entry point for this task.
        /// </summary>
        protected override void ExecuteTask()
        {
            try
            {
                if (_maxAge == TimeSpan.Zero && _maxCount == 0)
                {
                    Log(Level.Warning, "No constraints set on clean up task. Either maxage or maxcount must be set for clean up task to function.");
                    return;
                }

                int directoryDeleteCount = 0;
                int directoryKeepCount = 0;

                List<DirectoryInfo> possibleCleanUpDirectories = new List<DirectoryInfo>();
                foreach (string possibleCleanUpDirectoryName in _cleanUpFileSet.DirectoryNames)
                {
                    DirectoryInfo possibleCleanUpDirectory = new DirectoryInfo(possibleCleanUpDirectoryName);

                    // skip the base directory
                    if (!string.Equals(_cleanUpFileSet.BaseDirectory.FullName, possibleCleanUpDirectory.FullName, StringComparison.InvariantCultureIgnoreCase))
                    {
                        possibleCleanUpDirectories.Add(possibleCleanUpDirectory);
                    }
                }
                // sort by time descending to that the most recently created are at the top
                possibleCleanUpDirectories.Sort((DirectoryLeft, DirectoryRight) => DirectoryRight.CreationTimeUtc.CompareTo(DirectoryLeft.CreationTimeUtc));
                foreach (DirectoryInfo possibleCleanUpDirectory in possibleCleanUpDirectories)
                {
                    TimeSpan possibleCleanUpDirectoryAge = DateTime.UtcNow.Subtract(possibleCleanUpDirectory.CreationTimeUtc);
                    if (directoryKeepCount < _maxCount)
                    {
                        // Log(Level.Info, "Directory: {0}, Age: {1}", possibleCleanUpDirectory, possibleCleanUpDirectoryAge);
                        directoryKeepCount++;
                    }
                    else if (_maxAge != TimeSpan.Zero && possibleCleanUpDirectoryAge > _maxAge)
                    {
                        if (this.Verbose)
                            Log(Level.Info, "Deleted directory because it's age exceed the maximum allowed. Directory: {0}, Age: {1}", possibleCleanUpDirectory, possibleCleanUpDirectoryAge);
                        DeleteFileSystemInfo(possibleCleanUpDirectory);
                        directoryDeleteCount++;
                    }
                    else
                    {
                        if (this.Verbose)
                            Log(Level.Info, "Deleted directory because maximum count has been reached. Directory: {0}", possibleCleanUpDirectory);
                        DeleteFileSystemInfo(possibleCleanUpDirectory);
                        directoryDeleteCount++;
                    }
                }

                int fileDeleteCount = 0;
                int fileKeepCount = 0;

                List<FileInfo> possibleCleanUpFiles = new List<FileInfo>();
                foreach (string possibleCleanUpFileName in _cleanUpFileSet.FileNames)
                {
                    FileInfo possibleCleanUpFile = new FileInfo(possibleCleanUpFileName);
                    possibleCleanUpFiles.Add(possibleCleanUpFile);
                }
                // sort by time descending to that the most recently created are at the top
                possibleCleanUpFiles.Sort((fileLeft, fileRight) => fileRight.CreationTimeUtc.CompareTo(fileLeft.CreationTimeUtc));
                foreach (FileInfo possibleCleanUpFile in possibleCleanUpFiles)
                {
                    TimeSpan possibleCleanUpFileAge = DateTime.UtcNow.Subtract(possibleCleanUpFile.CreationTimeUtc);
                    if (fileKeepCount < _maxCount)
                    {
                        // Log(Level.Info, "File: {0}, Age: {1}", possibleCleanUpFile, possibleCleanUpFileAge);
                        fileKeepCount++;
                    }
                    else if (_maxAge != TimeSpan.Zero && possibleCleanUpFileAge > _maxAge)
                    {
                        if (this.Verbose)
                            Log(Level.Info, "Deleted file because it's age exceed the maximum allowed. File: {0}, Age: {1}", possibleCleanUpFile, possibleCleanUpFileAge);
                        DeleteFileSystemInfo(possibleCleanUpFile);
                        fileDeleteCount++;
                    }
                    else
                    {
                        if (this.Verbose)
                            Log(Level.Info, "Deleted file because maximum count has been reached. File: {0}", possibleCleanUpFile);
                        DeleteFileSystemInfo(possibleCleanUpFile);
                        fileDeleteCount++;
                    }
                }

                if ((directoryDeleteCount > 0 || directoryKeepCount > 0)
                    && (fileDeleteCount > 0 || fileKeepCount > 0))
                    Log(Level.Info, "Deleted {0} directories and {2} files. Kept {1} directories and {3} files.", directoryDeleteCount, directoryKeepCount, fileDeleteCount, fileKeepCount);
                else if (directoryDeleteCount > 0 || directoryKeepCount > 0)
                    Log(Level.Info, "Deleted {0} directories and kept {1} directories.", directoryDeleteCount, directoryKeepCount);
                else if (fileDeleteCount > 0 || fileKeepCount > 0)
                    Log(Level.Info, "Deleted {0} files and kept {1} files.", fileDeleteCount, fileKeepCount);
                else
                    Log(Level.Info, "No files were found to clean up.");
            }
            catch (Exception ex)
            {
                if (this.FailOnError)
                    throw new BuildException("cleanup task failed during execution due to an unhandled exception.", this.Location, ex);
                else
                    Log(Level.Error, "cleanup task failed during execution due to an unhanled exception. ex = {0}", ex);
            }
        }

        /// <summary>
        /// Deletes recursively a directory or an individual file.
        /// </summary>
        /// <param name="fsi"></param>
        /// <remarks>Needed a custom method to handle read only files or directory since built-in methods do not.</remarks>
        private void DeleteFileSystemInfo(FileSystemInfo fsi)
        {
            // very important to set the directory/file to normal before we try to delete
            // as the Delete method will error out when in read only
            fsi.Attributes = FileAttributes.Normal;
            var di = fsi as DirectoryInfo;

            if (di != null)
            {
                foreach (var dirInfo in di.GetFileSystemInfos())
                    DeleteFileSystemInfo(dirInfo);
            }

            DateTime retryStopDateTime = DateTime.Now.Add(this._retryTimeout);
            Exception lastException = null;
            bool deleteSucceed = false;
            while (DateTime.Now < retryStopDateTime
                && !deleteSucceed)
            {
                try
                {
                    // ensure the file is still there by refreshing
                    fsi.Refresh();

                    // is it still there then let's try deleting it
                    if (fsi.Exists)
                        fsi.Delete();

                    // need to keep track of whether we did delete successfully
                    deleteSucceed = true;

                    // terminate the loop because either the file doesn't exist
                    // or we've issued the delete command successfully
                    break;
                }
                catch (Exception ex)
                {
                    lastException = ex;
                    string exception = (this.Verbose) ? ex.ToString() : ex.Message;
                    Log(Level.Warning, "Warning: Unable to delete file or directory {0}. Exception = {1}", fsi.FullName, exception);
                    System.Threading.Thread.Sleep(5000);
                    Log(Level.Info, "Retrying delete of {0}...", fsi.FullName);
                }
            }

            // throw an exception if we're unable to delete the file
            if (!deleteSucceed)
            {
                throw new ApplicationException(string.Format("Unable to successfully delete {0}.", fsi.FullName), lastException);
            }
        }
    }
}
